'''
This is a check list to be used when reviewing a UI test. The objective is to improve 
the quality of the UI tests promoted to our ci.
'''

import robot.parsing.model

from string import Template, lower

from rsa.rule import Rule, RuleViolation, TemplateRule
from rsa.report import ReportManager
from rsa.util.common_utilities import violation_reported, load_option

name = "UI"

description = __doc__

report = ReportManager()

#
# Naming convention rules
#

class CheckUINameConvention(Rule):
    """
    This rule verifies if proper naming convention for test cases is satisfied (e.g., UI_0001_DescriptiveName)
    """

    rule_id = "CheckUINameConvention"
    name = "Check UI name convention"
    message = Template("Invalid test name, $item1")
    solution = Template("Test name format should be UI_XXXX_TESTNAME")
    example = '''
        # Invalid UI test name
        FlightStatusDisabled
        # Valid UI test name
        UI_0006_FlightStatusDisabled
    '''
    severity = 1
    description = "CheckUINameConvention find instances where the naming convention for test cases is not satisfied"
    
    def apply(self, node, symbols):
        # check only test names, ignore resources or keywords
        if(isinstance(node, robot.parsing.model.UserKeyword)): return
        # check if the minimum test name length is satisfied 
        if(len(node.name) <= 7): report.add_rule_violation(RuleViolation(self, None, "", node))
        # check if test name convention is satisfied
        name = node.name
        p1 = name[:2]   # should be UI
        p2 = name[2]    # should be _
        p3 = name[3:7]  # should be a four digit number
        p4 = name[7]    # should be _
        p5 = name[8:]   # should be the name of the case without spaces
        if(p1 != "UI"):
            return report.add_rule_violation(RuleViolation(self, None, ["first two letters should be UI not '" + p1 + "'"], node))
        if(p2 != "_"):
            return report.add_rule_violation(RuleViolation(self, None, ["UI should be followed by '_' not '" + p2 + "'"], node))
        if(not p3.isdigit()):
            return report.add_rule_violation(RuleViolation(self, None, ["UI_ should be followed by a four digit number, not '" + p3 + "'"], node))
        if(p4 != "_"):
            return report.add_rule_violation(RuleViolation(self, None, [p1 + p2 + p3 + " should be followed by '_' not '" + p4 + "'"], node))
        if(p5.find(" ") != -1):
            return report.add_rule_violation(RuleViolation(self, None, ["test name should not have spaces."], node))

#
# Documentation convention rules
#

class CheckDocumentationConvention(Rule):
    """
    This rule verifies that documentation is written at the test suite level and at the test case level as well
    """

    rule_id = "CheckDocumentationConvention"
    name = "Check documentation convention"
    message = Template("Documentation missing in '$item1'")
    solution = Template("Add the proper documentation.")
    example = '''
        # Proper documentation convention
        Robot.Air.Air Traveler Info
            \ Documentation: documentation about "Air traveler info" testing
            * UI_006_FlightStatusDisabled
                \ Documentation: documentation about this particular test case 
    '''
    severity = 2
    description = "CheckDocumentationConvention verifies that documentation is written at the test suite level and at the test case level as well"
    
    def apply(self, node, symbols):
        # check if documentation is written at the test case level
        if(node.doc.comment == None):
            report.add_rule_violation(RuleViolation(self, None, [node.name], node))
        # check if documentation is written at the test suite level
        if(node.parent.parent.setting_table.doc.comment == None):
            if(not violation_reported(report.report, self.name, node.parent.parent, [node.parent.parent.name])):
                report.add_rule_violation(RuleViolation(self, None, [node.parent.parent.name], node.parent.parent))

#                
# Tagging convention rules    
#
               
class NoSpacesInTagNames(Rule):
    """
    This rule verifies that there are no spaces in tag names
    """
    
    rule_id = "NoSpacesInTagNames"
    name = "No spaces in tag names"
    message = Template("No spaces are allowed in tags: '$item1'")
    solution = Template("Remove spaces")
    example = '''
        # Invalid tag name
        Air QDU selfreferrer WA-233
        # Valid tag name
        Air_QDU_selfreferrer_WA-233
    '''
    severity = 0
    description = "NoSpacesInTagNames verifies that there are no spaces in tag names "
    
    def apply(self, node, symbols):
        # check only test names, ignore resources or keywords
        if(isinstance(node, robot.parsing.model.UserKeyword)): return
        if(node.tags.value == None): return
        if(len(node.tags.value) > 0):
            for tag in node.tags.value:
                if(" " in tag):
                    report.add_rule_violation(RuleViolation(self, None, [tag], node))
                    
class StandardTestSuiteTags(Rule):
    """
    This rule verifies that test suites have a force tag with same name as the suite (without spaces)  
    """
    
    rule_id = "StandardTestSuiteTags"
    name = "Standard test suite tags"
    message = Template("Test suite '$item1' should have a tag named '$item2'")
    solution = Template("Add tag '$item2'")
    example = '''
        # A test suite with the mandatory force tag
        EBK Car OBP
            / force tag: EBKCarOBP
            * UI_0001_EBK_OPBP_Guest User_CarUpgrade_Verification
    '''
    severity = 1
    description = "StandardTestSuiteTags verifies that test suites have a force tag with same name as the suite (without spaces)"
    suites = ['Robot']    
    
    def apply(self, node, symbols):
        # check only test names, ignore resources or keywords
        if(isinstance(node, robot.parsing.model.UserKeyword)): return
        # evaluate test suites
        self.evaluate_suite(node.parent.parent)
        # evaluate test directories
        self.evaluate_suite(node.parent.parent.parent)
        
    def evaluate_suite(self, suite):
        """
        Validate if a test suite contains a force tag with the same name as the suite 
        """
        if(suite == None): return
        if(suite.name in self.suites): return
        valid = False
        name = suite.name.replace(" ", "")
        if(suite.setting_table.force_tags.value != None):
            for tag in suite.setting_table.force_tags.value:
                if(tag.lower() == name.lower()):
                    valid = True
        if(not valid):
            report.add_rule_violation(RuleViolation(self, None, [suite.name, name], suite))
        self.suites.append(suite.name)
        
class CommonTagInSuite(Rule):
    """
    This rule finds suites where all the test cases have the same tag.
    """
    
    rule_id = "CommonTagInSuite"
    name = "Common tag in suite"
    message = Template("Tag/s '$item1' were found in all the test cases of the suite '$item2'")
    solution = Template("Add force tag/s '$item1' in suite '$item2', and remove it from the test cases.")
    example = '''
    '''
    severity = 2
    description = "CommonTagInSuite finds suites where all the test cases have the same tag."
    suites = []
    valid_tags = []
    
    def __init__(self):
        self.tags = load_option(self.rule_id)
    
    def apply(self, node, symbols):
        # check only test names, ignore resources or keywords
        if(isinstance(node, robot.parsing.model.UserKeyword)): return
        #
        if(node.parent.parent.name in self.suites): return
        #
        suite = node.parent.parent
        if(suite.testcase_table.tests == None): return
        tests = suite.testcase_table.tests
        if(tests[0].tags.value == None): return
        common_tags = tests[0].tags.value
        for k in range(1, len(tests)):
            common_tags = [i for i in common_tags if tests[k].tags.value != None 
                           and i in tests[k].tags.value]
            if(len(common_tags) == 0):
                break
        common_tags = self.remove_valid_tags(common_tags)
        if(len(common_tags) > 0):
            tags = ",".join(common_tags)
            report.add_rule_violation(RuleViolation(self, None, [tags, suite.name], suite))
        self.suites.append(suite.name)
        
    def remove_valid_tags(self, common):
        for tag in self.tags:
            if(tag in common):
                common.remove(tag)
        return common
        
class DocumentationOnSpecificTags(Rule):
    """
    This rule verifies that test cases with specific tags contains the proper documentation.
    """
    rule_id = "DocumentationOnSpecificTags"
    name = "Documentation on specific tags"
    message = Template("Documentation must exist for tests tagged as '$item1'")
    solution = Template("'$item2'")
    example = '''
        # Valid tag documentation
        Air Traveler Info.UI_006_FlightStatusDisabled
            / Tag: in-progress
            / Documentation: documentation outlining what remains to be done or fixed for that test
    '''
    severity = 1
    description = "DocumentationOnSpecificTags verifies that test cases with specific tags contains the proper documentation."
    tags = []
    
    def __init__(self):
        self.tags = load_option(self.rule_id, ['live-only', 'in-progress', 'known-issue'])

    def apply(self, node, symbols):
        # check only test cases, ignore resources or keywords
        if(isinstance(node, robot.parsing.model.UserKeyword)): return
        if(not node.tags.value): return
        # check if documentation exists for test tagged as 'live-only', 'in-progress'
        # and 'known-issue'
        for tag in node.tags.value:
            if(tag.lower() in self.tags):
                if(len(node.doc.value) == 0):
                    msg = "Add a comment or documentation explaining the test"
                    if(tag == "live-only"):
                        msg = "Add a comment or documentation explaining what needs to be done."
                    if(tag == "in-progress"):
                        msg = "Add comments or documentation outlining what remains to be done or fixed for that test."
                    if(tag == "known-issue"):
                        msg = "Add comments or documentation linking to a Jira issue."
                    report.add_rule_violation(RuleViolation(self, None, [tag, msg], node))
                    
#                
# Code convention rules    
#
      
class NoSynchronizationSteps(TemplateRule):
    """
    This rule looks for synchronization steps in the test cases.
    """
    rule_id = "NoSynchronizationSteps"
    name = "No synchronization steps"
    message = Template("Avoid the use of 'Sleep'.")
    solution = Template("Where possible instead of 'Sleep' use 'Wait Until Page Contains Element', 'Wait Until Keyword Succeeds', or 'Wait For Page Initialization.'")
    example = '''
        # Avoid the use of Sleep keyword
        Open Browser | http://localhost:7585 | firefox      
        Sleep | 10s          
    '''
    severity = 1
    description = "NoSynchronizationSteps looks for synchronization steps in the test cases."
    sync_keyword = "Sleep"
    
    def visit_step(self, step):
        if(step.keyword == self.sync_keyword):
            report.add_rule_violation(RuleViolation(self, step, [step.keyword], self.node, self.line))
                    
    def visit_loop(self, loop):
        for step in loop.steps:
            self.line += 1
            self.visit(step)
            
class NotProperlyExistenceVerification(TemplateRule):
    """
    This rule checks that presence verification of elements is done according to the recommended practices. 
    """
    
    rule_id = "NotProperlyExistenceVerification"
    name = "Not properly existence verification"
    message = Template("Keyword $item1")
    solution = Template("Use $item2 with a 'timeout' parameter. This practice improves test reliability.")
    example = '''
        # Avoid using this keyword
        Page Should Contain Element  |  LeftRailLocator
        # Replace the keyword like this
        Wait Until Page Contains Element  |  LeftRailLocator | Timeout  
    '''
    severity = 1
    description = "NotProperlyExistenceVerification verifies that keywords, which wait for the presence of elements with a timeout, are actually used in every test requiring existence checking."
    target_keyword_prefix = "pageshouldcontain"
    right_keyword = "waituntilpagecontainselement"
    
    def keyword_maches_target(self, keyword):
        if keyword != None:
            plain_keyword = lower(keyword.replace(' ', '')) 
            if (plain_keyword.startswith(self.target_keyword_prefix)):return 1;
            else: return 0;
        return 0
    
    def is_right_keyword(self, keyword):
        if keyword != None:
            plain_keyword = lower(keyword.replace(' ', ''))
            if (plain_keyword == self.right_keyword):return 1;
            else: return 0;
        return 0
    
    def visit_step(self, node):
        # check if the keyword matches the target that has to be replaced
        if(self.keyword_maches_target(node.keyword)):
            report.add_rule_violation(RuleViolation(self, node, ["'"+ node.keyword+"' should not be used for presence verification", "'Wait Until Page Contains Element'"], self.node, self.line))
        # check if the right keyword is used properly with a timeout parameter
        elif (self.is_right_keyword(node.keyword)):
            if(len(node.args) < 2):
                report.add_rule_violation(RuleViolation(self, node, ["'"+ node.keyword+"' requires 'locator' and 'timeout' parameters", "'Wait Until Page Contains Element'"], self.node, self.line))
                
    def visit_loop(self, loop):
        for step in loop.steps:
            self.line += 1
            self.visit(step)            
        
#                
# Locator convention rules    
#
                    
class NoXPathExpressions(TemplateRule):
    """
    This rule check locators to make sure there are no xpath expressions.
    """
    
    rule_id = "NoXPathExpressions"
    name = "No xpath expressions"
    message = Template("There should't be any xpath expressions: '$item1', only context/component mapping")
    solution = Template("Add to locator mapping update request wiki to notify UI dev if new mapping is required.")
    example = '''
        # Avoid the usage of xpath expression in test cases
        Page Should Contain Element | //meta[@name='DCSext.qdu']
        # Use keywords to encapsulate xpath expression 
        Page Should Contain Element | PageElementDCSext
    '''
    severity = 0
    description = "NoXPathExpressions check locators to make sure there are no xpath expressions."
    xpathchar = ["\\", "/"]
    validkeywords = ["Go To"]
       
    def visit_step(self, step):
        # return if there are no arguments
        if(len(step.args) > 0):
            for arg in step.args:
                if(len(arg) > 0 and arg[0] in self.xpathchar):
                    if(not (step.keyword in self.validkeywords)):
                        report.add_rule_violation(RuleViolation(self, step, [arg], self.node, self.line))                    
                    
    def visit_loop(self, loop):
        for step in loop.steps:
            self.line += 1
            self.visit(step)
            
class LocatorsOnlyInKeywords(TemplateRule):
    """
    This rule verifies that locators are used primarily in keywords only and avoid using them in the UI test itself
    """
    
    rule_id = "LocatorsOnlyInKeywords"
    name = "Locators only in keywords"
    message = Template("Locator should only be used in keywords: '$item1'")
    solution = Template("Move the locator form the UI test to a shared keyword.")
    example = '''
        # Avoid the use of locators
        Page Should Contain Element  |  lc=leftRail
        # Move the locator to a shared keyword
        Page Should Contain Element  |  LeftRailLocator 
    '''
    severity = 1
    description = "LocatorsOnlyInKeywords verifies that locators are used primarily in keywords"
    locator_start = "lc="
       
    def visit_step(self, node):
        # check only test cases, ignore resources or keywords
        if(isinstance(self.node, robot.parsing.model.UserKeyword)): return
        # if argument is a locator report violation
        if(len(node.args) > 0):
            for arg in node.args:
                if(len(arg) > 4 and arg[0:3] == self.locator_start):
                    report.add_rule_violation(RuleViolation(self, node, [arg], self.node, self.line))
                    
    def visit_loop(self, loop):
        for step in loop.steps:
            self.line += 1
            self.visit(step)
